<?php
/**
* This file is part of Webappkit, a packages system for PHP web apps
* @link http://webappkit.net
* @package webappkit
* @author J.Ducastel <jeremie@ducastel.name>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

if (class_exists('Webappkit')) {
	// echo "<p>webappkit defined yet !</p>";
	return null;
}

/**
 * exceptions
 */
class WakException extends Exception {}
// a kit is malformed
class WakBadKit extends WakException {}
// a config file is broken or missing
class WakBadCfg extends WakException {}
// PHP version is out of functional range
class WakBadPHP extends WakException {}
// a file is missing
class WakMissingFile extends WakException {}
// a class is missing
class WakMissingClass extends WakException {}
// a kit is missing
class WakMissingKit extends WakException {}
// method/feature not implemented
class WakNotImplemented extends WakException {}

/**
* Webappkit "kit" manipulation class.
* Kits are packages for PHP web applications.
* Has static utility methods, and instance methods
* @link http://code.google.com/p/webappkit/wiki/Kits
* @todo use exceptions
*/
class Webappkit {
/*--------------------------------------------------------------------
 STATIC METHODS
--------------------------------------------------------------------*/

	/**
	* factory : use this to get a kit instance.
	* @param string $id kit id : use / to access subkits
	* @static
	* @return webappkit
	*/
	static function & getKit($id) {
		// cleaning id
		$id=Webappkit::checkId($id);
		// storing instances in a static array
		static $kits=array();
		if (!isset($kits[$id])) {
			// loading kit libraries
			webappkit::loadKit($id);
			// loading kit config
			$cfg=webappkit::getCfgFromId($id);
			// defining kit's class (default is webappkit)
			$class=isset($cfg['kit']['class'])?$cfg['kit']['class']:'webappkit';
			// building instance of correct class
			if (!class_exists($class))
				throw new WakBadKit("Kit class ($class) does not exists for kit ($id)");
			$kit=&new $class($id);
			if (!is_a($kit,'webappkit'))
				// the specified class does not extend webappkit !
				throw new WakBadKit("Class ($class) does not extends webappkit");
			// storing instance in cache
			$kits[$id]=&$kit;
		}
		return $kits[$id];
	}

	/**
	 * loads a kit libraries and dependancies
	 * @access public
	 * @static
	 * @param string $id
	 * @return string loaded version string
	 */
	static function loadKit($id) {
		// cleaning id
		$id=Webappkit::checkId($id);
		// using a cache to avoid multiple loadings
		static $loaded=array();
		if ($id and !isset($loaded[$id])) {
			// first time, loading
			$path=webappkit::getPathFromId($id);
			// reading meta/config
			$cfg=webappkit::getCfgFromId($id);
			// checking for supported php version
			if (isset($cfg['kit']['phpversion'])
			and !webappkit::isVersionWithinRange(phpversion(),$cfg['kit']['phpversion']))
				throw new WakBadPHP("Kit $id does not support PHP version ".phpversion());
			// loading external libraries (kits dependancies)
			if (is_array($cfg['load_kits'])) {
				foreach ($cfg['load_kits'] as $required) {
					// parsing require statement : "id version-range"
					$kit=$required;
					if ($kit==$id) {
						// kit requires iself, preventing infinite loop
						continue;
					}
					try {
						Webappkit::loadkit($kit);
					} catch (WakException $e) {
						// dependency failing
						throw new WakMissingKit("Kit $kit is required by $id, could nto be loaded because: ".$e->getMessage());
					}
				}
			}
			// loading internal libraries
			if (is_array($cfg['load_files'])) {
				foreach ($cfg['load_files'] as $file)
					webappkit::loadlibfile($path.$file);
			}
			// indexing "optional" library classes for autoload
			if (is_array($cfg['load_classes'])) {
				foreach ($cfg['load_classes'] as $class => $file) {
					Webappkit::$classes[$class]=$path.$file;
					//webappkit::loadlibfile($path.$file);
				}
			}
			// loading subkits
			if (is_array($cfg['load_subkits'])) {
				foreach ($cfg['load_subkits'] as $subkit) {
					webappkit::loadkit($id.'/'.$subkit);
				}
			}
			$version=isset($cfg['kit']['version'])?$cfg['kit']['version']:'0.0.undefined';
			$loaded[$id]=$version;
			// return true;
		}
		return $loaded[$id];
	}

	/**
	* returns spent time in microseconds, with chosen precision
	* if not defined, defines WAK_START_TIME as reference microtime
	* @param int $precision (default 5)
	* @static
	* @access public
	* @return float spent time since the first call
	*/
	static function timeCounter($precision=5) {
		list($usec, $sec) = explode(" ", microtime());
		$microtime=((float)$usec + (float)$sec);
		if (!defined('WAK_START_TIME'))
			define('WAK_START_TIME',$microtime);
		//echo $microtime;
		$timeCounter=$microtime-WAK_START_TIME;
		return round($timeCounter,$precision);
	}

	/**
	 * lists kits in a path. Uses root path as default
	 * @static
	 * @param string $path
	 * @return array kits ids
	 */
	static function listKits($path=null) {
		if (is_null($path))
			$path=WAK_ROOT_PATH;
		if (!is_dir($path))
			return false;
		$path=realpath($path);
		$kits=array();
		$dir=dir($path);
		while ($file=$dir->read()) {
			if (is_dir($path.'/'.$file)
			and substr($file,-4,4)=='.wak') {
				// looks like a kit
				$id=substr($file,0,-4);
				$kits[]=$id;
			}
		}
		return $kits;
	}

	/**
	 * Checks a kit id for usability
	 * checks id validity
	 * then folder existence, then wak.ini file
	 * SHOULD NOT raise any exception
	 * @static
	 * @param string $id
	 * @return bool true if kit exists and is valid, false elsewhere
	 */
	function checkKit($id) {
		try {
			$id=Webappkit::checkId($id);
			$path=Webappkit::getPathFromId($id);
			if (!file_exists($path.'wak.ini'))
				return false;
			return true;
		}  catch (Exception $e) {
			return false;
		}
	}

	/**
	* checks/cleans kit ID string format
	* accepts both / and . as separators for nesting levels
	* converts / to . in output
	* @static
	* @access public
	* @param string
	* @return string or false if id is invalid
	*/
	function checkID($id) {
		$id=str_replace('/','.',strtolower($id));
		if (!ereg('^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$',$id))
			return false;
		return $id;
	}

	/**
	* get kit's physical path from kit id
	* @static
	* @access public
	* @param string $id
	* @return string path with trailing slash
	*/
	static function getPathFromId($id) {
		if (!$id=Webappkit::checkID($id))
			throw new WakBadKit("invalid path {$path} for id {$id}");
			// return false;
		$levels=explode('.',$id);
		$path='';
		foreach ($levels as $l)
			$path.="$l.wak/";
		$path=WAK_ROOT_PATH.$path;
		if (!file_exists($path)) {
			// trigger_error("invalid path {$path} for id {$id}");
			throw new WakBadKit("invalid path {$path} for id {$id}");
			return false;
		}
		return $path;
	}

	/**
	* Recursive array merge, replacing values.
	* merges recursively array_1 with array_2, replacing array_1 keys by array_2 keys on collisions.
	* @static
	* @param array &$array_1
	* @param array &$array_2
	* @return &array merged result
	*/
	static function & mergeArrays(&$array_1,&$array_2) {
		$merge=$array_1;
		$keys_1=array_keys($array_1);
		$keys_2=array_keys($array_2);
		foreach ($merge as $key => $value) {
			if (!isset($array_2[$key]))
				continue;
			if (is_array($value) and is_array($array_2[$key])) {
				// appel recursif
				$merge[$key]=Webappkit::mergeArrays($value,$array_2[$key]);
			} else {
				$merge[$key]=$array_2[$key];
			}
		}
		foreach ($array_2 as $key => $value)
			if (!isset($merge[$key])) $merge[$key]=$value;
		return $merge;
	}

	/**
	* loads package/kit metadata from ini file
	* @access public
	* @param string $id
	* @return array
	* @todo checking content format
	*/
	static function getCfgFromId($id) {
		// using a cache to avoid multiple loadings
		static $cfgs=array();
		if (!isset($cfgs[$id])) {
			// default config
			$default=array(
				'kit'=>array(
					'version'=>'0.0'
					, 'desc'=>'There should be a short description there')
				,'load_kits'=>array()
				,'load_files'=>array()
				,'load_classes'=>array()
				,'load_subkits'=>array()
				,'tools'=>array());
			// getting kit's path
			$path=webappkit::getPathFromId($id);
			// getting ini file
			$file=$path.'wak.ini';
			if (!file_exists($file) or !is_file($file))
				throw new WakBadCfg("kit wak.ini file $file not found");
			// parsing
			if (!$cfg=parse_ini_file($file,true))
				throw new WakBadCfg("failed to parse $file");
			// cfg ok
			$cfg=Webappkit::mergeArrays($default,$cfg);
			$cfgs[$id]=$cfg;
		}
		return $cfgs[$id];
	}

	/**
	 * loads a library file.
	 * can switch file upon current php version : if $file ends by php*, will load .php4, .php5 etc
	 * @param string $file full physical path
	 * @return bool
	 */
	static function loadLibFile($file) {
		// auto php version ?
		if (substr($file,-1,1)=='*') {
			$php=substr(phpversion(),0,1);
			$file=substr($file,0,-1).$php;
		}
		// checking existence
		if (!file_exists($file)) {
			// trigger_error("can't include file $file, don't exist");
			throw new WakMissingFile("can't include file $file, don't exist",500);
			return false;
		}
		Webappkit::$loaded_size+=filesize($file);
		// echo "<p>".Webappkit::$loaded_size.'</p>';
		include_once($file); // echo "<p>included $file</p>";
		return true;
	}

	/**
	 * loads a class definition file
	 * it has to be indexed within a kit's [load_classes] section
	 * @param string $class class name
	 * @return bool
	 */
	static function loadClass($class) {
		$class=strtolower($class);
		if (!isset(Webappkit::$classes[$class])) {
			return false;
		}
		return Webappkit::loadLibFile(Webappkit::$classes[$class]);
	}

	/**
	 * checks that a version string is within version range
	 * @static
	 * @access public
	 * @param string $version version string to check x or x.y or x.y.* etc
	 * @param string $range version range min-max or minAndMax or min+ or max-
	 * @return bool true if in range, false elsewhere
	 */
	static function isVersionWithinRange($version,$range) {
		// parsing range
		switch (true) {
			case ereg('^[0-9\.\*]+$',$range): // minAndMax (with *=jokers)
				$min=$range; $max=$range;
				break;
			case substr($range,-1,1)=='+': // min+
				$min=substr($range,0,-1);
				$max=null;
				break;
			default: // min-max
				$range=explode('-',$range);
				$min=$range[0];
				$max=isset($range[1])?$range[1]:null;
		}
		// echo "<p>$min <= $version <= $max ?</p>";
		// is under minimum ?
		if (webappkit::cmpVersionStrings($version,$min)<0)
			return false; // under min
		// is over max ?
		if ($max and webappkit::cmpVersionStrings($max,$version)<0)
			return false;
		// is within range
		return true;
	}

	/**
	 * compares version strings
	 * @static
	 * @access public
	 * @param string $version1
	 * @param string $version2
	 * @return int 1 if $version1>$version2, 0 if $version1==$version2, -1 if $version1<$version2
	 */
	static function cmpVersionStrings($version1,$version2) {
		$v1=explode('.',$version1);
		$v2=explode('.',$version2);
		// looping through segments
		for ($n=0; $n<count($v1); $n++) {
			switch (true) {
				case ($v1[$n]=='*'): // joker, versions match
					return 0;
				case !isset($v1[$n]): // v2 longer than v1, so over
					return -1;
				case !isset($v2[$n]): // v1 longer than v2, so over
					return 1;
				case $v1[$n]>$v2[$n]: // v1 over v2
					return 1;
				case $v1[$n]<$v2[$n]: // v2 over v1
					return -1;
				case $v1[$n]==$v2[$n]: // segment match, continue
					continue;
			}
		}
		// versions are identical
		return 0;
	}

/*--------------------------------------------------------------------
 INSTANCE METHODS
--------------------------------------------------------------------*/

	/**
	 * constructor
	 * @access private
	 * @param string kit id
	 * @todo do not build tools, use lazy loading with __get instead
	 */
	function __construct($id) {
		// setting config
		$this->_id=$id;
		$this->_path=webappkit::getPathFromId($id);
		$this->_cfg=webappkit::getCfgFromId($id);
		// setting tool members
		// deported to __get for lazy loading
		/*foreach ($this->_cfg['tools'] as $name => $class) {
			$this->_buildTool($name,$class);
		}*/
	}

	/**
	 * this method is the one to be implemented and used for requests handling kits
	 * @access public
	 * @return bool
	 */
	public function run() {
		throw new WakNotImplemented('kit '.$this->getId().' is not able to handle requests. method run() not implemented.');
		return false;
	}

	/**
	 * get kit's path
	 * @return string
	 */
	public function getPath() {
		return $this->_path;
	}

	/**
	 * get kit's id
	 */
	public function getId() {
		return $this->_id;
	}

	/**
	 * get kit's config
	 * @return array
	 */
	public function getCfg($key=null) {
		return $this->_cfg;
	}

	/**
	 * list subkits
	 * @access public
	 * @return array kits ids
	 */
	function listSubkits() {
		return Webappkit::listKits($this->getPath());
	}

	/**
	 * get a subkit instance if available
	 * @access public
	 * @param string subkit id (without the parent part)
	 * @return Webappkit
	 */
	function getSubkit($id) {
		return Webappkit::getKit($this->getId().'/'.$id);
	}

	/**
	 * get kit tools.
	 * May filter by class
	 * @access public
	 * @param string $class if provided, will return only tools from this class
	 * @return array name=>instance
	 */
	public function & getTools($class=null) {
		$tools=array();
		// listing members
		$names=array_keys(get_object_vars($this));
		// print_r($names);
		if ($class) {
			// looping members
			foreach ($names as $name) {
				// echo $name.' : '; print_r($this->$name);
				// checking class
				if (is_a($this->$name,$class))
					$tools[$name]=&$this->$name;
			}
		} else {
			// looping members
			foreach ($names as $name) {
				// checking class
				if (is_a($this->$name,'wakTool'))
					$tools[$name]=&$this->$name;
			}
		}
		return $tools;
	}

	/**
	 * tells if a tool has its custom config
	 * @access public
	 * @param string $tool tool's name
	 * @return bool
	 */
	public function toolHasConfig($tool) {
		$file=$this->getPath().$tool.'.tool.ini'; // echo $file;
		return file_exists($file) and is_file($file);
	}

/*--------------------------------------------------------------------
 MEMBERS
--------------------------------------------------------------------*/
	/**
	 * @var string kit unique id
	 */
	protected $_id;

	/**
	 * @var array kit configuration data
	 */
	protected $_cfg=array();

	/**
	 * @var string physical path
	 */
	protected $_path;

	/**
	 * @var array tool instances
	 */
	// var $tools=array();

	/**
	 * @var int total loaded weight, in octets
	 */
	public static $loaded_size=0;

	/**
	 * @var array classes index for __autoload class => path
	 */
	public static $classes=array();

/*--------------------------------------------------------------------
PRIVATE / PROTECTED METHODS
--------------------------------------------------------------------*/

	/**
	 * getter for tools. autoload tools when required
	 * @param
	 */
	public function & __get($name) {
		if (substr($name,0,1)!='_'
		and !isset($this->$name)
		and isset($this->_cfg['tools'][$name])) {
			$this->_buildTool($name,$this->_cfg['tools'][$name]);
		}
		return $this->$name;
	}

	/**
	* loads tool's config ini file ( {name}.tool.ini )
	* @access protected
	* @param string $name tool's name
	* @return array|null|false
	*/
	protected function _getToolCfg($name) {
		// getting ini file
		$file=$this->getPath().$name.'.tool.ini'; // echo $file;
		if (!file_exists($file) or !is_file($file)) {
			return null;
		}
		// parsing, with sections
		$cfg=parse_ini_file($file,true);
		if (!is_array($cfg)) {
			//trigger_error("failed to parse $file");
			throw new WakBadCfg("failed to parse $file",500);
			return false;
		}
		return $cfg;
	}

	/**
	 * builds a tool member : create a tool member instance for the kit.
	 * Will try to load config from ini file
	 * @access protected
	 * @param string $name the member name given to tool
	 * @param string $class
	 * @return bool
	 */
	protected function _buildTool($name,$class) {
		// defining class
		if (!class_exists($class)) {
			trigger_error("failed to build tool $name for kit {$this->_id} : class $class is missing",E_USER_WARNING);
			return false;
		}
		// trying to read config from ini file within self kit
			$cfg=$this->_getToolCfg($name);
		// calling factory method
			// $instance=$class::factory($this,$name);
		$instance=call_user_func(array($class,'Factory'),$this->_id,$cfg);
		$this->$name=$instance;
		return true;
	}
}

/**
 * auto-loading of indexed classes
 * @param string $class class name
 */
function __autoload($class) {
	return Webappkit::loadClass($class);
}
